Panduan komprehensif untuk API createPortal React, mencakup teknik pembuatan portal, strategi penanganan event, dan kasus penggunaan tingkat lanjut untuk membangun UI yang fleksibel dan mudah diakses.
React createPortal: Menguasai Pembuatan Portal dan Penanganan Event
Dalam pengembangan web modern dengan React, menciptakan antarmuka pengguna yang terintegrasi secara mulus dengan struktur dokumen yang mendasarinya sangatlah penting. Meskipun model komponen React unggul dalam mengelola DOM virtual, terkadang kita perlu merender elemen di luar hierarki komponen normal. Di sinilah createPortal hadir. Panduan ini menjelajahi createPortal secara mendalam, mencakup tujuan, penggunaan, dan teknik tingkat lanjut untuk menangani event dan membangun elemen UI yang kompleks. Kita akan membahas pertimbangan internasionalisasi, praktik terbaik aksesibilitas, dan kesalahan umum yang harus dihindari.
Apa itu React createPortal?
createPortal adalah API React yang memungkinkan Anda merender anak-anak (children) dari komponen React ke bagian lain dari pohon DOM, di luar hierarki komponen induknya. Ini sangat berguna untuk membuat elemen seperti modal, tooltip, dropdown, dan overlay yang perlu diposisikan di tingkat teratas dokumen atau di dalam wadah tertentu, terlepas dari di mana komponen yang memicunya berada di dalam pohon komponen React.
Tanpa createPortal, mencapai hal ini sering kali melibatkan solusi rumit seperti memanipulasi DOM secara langsung atau menggunakan pemosisian absolut CSS, yang dapat menyebabkan masalah dengan konteks tumpukan (stacking contexts), konflik z-index, dan aksesibilitas.
Mengapa Menggunakan createPortal?
Berikut adalah alasan utama mengapa createPortal adalah alat yang berharga dalam persenjataan React Anda:
- Struktur DOM yang Lebih Baik: Menghindari penyarangan komponen yang terlalu dalam di DOM, menghasilkan struktur yang lebih bersih dan lebih mudah dikelola. Ini sangat penting untuk aplikasi kompleks dengan banyak elemen interaktif.
- Styling yang Disederhanakan: Dengan mudah memposisikan elemen relatif terhadap viewport atau wadah tertentu tanpa bergantung pada trik CSS yang rumit. Ini menyederhanakan styling dan tata letak, terutama saat berurusan dengan elemen yang perlu menimpa konten lain.
- Aksesibilitas yang Ditingkatkan: Memfasilitasi pembuatan UI yang dapat diakses dengan memungkinkan Anda mengelola fokus dan navigasi keyboard secara independen dari hierarki komponen. Misalnya, memastikan fokus tetap berada di dalam jendela modal.
- Penanganan Event yang Lebih Baik: Memungkinkan event untuk merambat (propagate) dengan benar dari konten portal ke pohon React, memastikan bahwa event listener yang terpasang pada komponen induk tetap berfungsi seperti yang diharapkan.
Penggunaan Dasar createPortal
API createPortal menerima dua argumen:
- Node React (JSX) yang ingin Anda render.
- Elemen DOM tempat Anda ingin merender node tersebut. Elemen DOM ini sebaiknya sudah ada sebelum komponen yang menggunakan
createPortaldi-mount.
Berikut adalah contoh sederhana:
Contoh: Merender Modal
Katakanlah Anda memiliki komponen modal yang ingin Anda render di akhir elemen body.
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const modalRoot = document.getElementById('modal-root'); // Mengasumsikan Anda memiliki <div id="modal-root"></div> di HTML Anda
if (!modalRoot) {
console.error('Elemen root modal tidak ditemukan!');
return null;
}
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
modalRoot
);
}
export default Modal;
Penjelasan:
- Kita mengimpor
ReactDOMkarenacreatePortaladalah metode dari objekReactDOM. - Kita mengasumsikan bahwa ada elemen DOM dengan ID
modal-rootdi HTML Anda. Di sinilah modal akan dirender. Pastikan elemen ini ada. Praktik umum adalah menambahkan<div id="modal-root"></div>tepat sebelum tag penutup</body>di fileindex.htmlAnda. - Kita menggunakan
ReactDOM.createPortaluntuk merender JSX modal ke dalam elemenmodalRoot. - Kita menggunakan
e.stopPropagation()untuk mencegah eventonClickpada konten modal memicu handleronClosepada overlay. Ini memastikan bahwa mengklik di dalam modal tidak akan menutupnya.
Penggunaan:
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>Buka Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Konten Modal</h2>
<p>Ini adalah konten dari modal.</p>
<button onClick={() => setIsModalOpen(false)}>Tutup</button>
</Modal>
</div>
);
}
export default App;
Contoh ini menunjukkan cara merender modal di luar hierarki komponen normal, memungkinkan Anda untuk memposisikannya secara absolut di halaman. Menggunakan createPortal dengan cara ini menyelesaikan masalah umum dengan konteks tumpukan (stacking contexts) dan memungkinkan Anda dengan mudah membuat styling modal yang konsisten di seluruh aplikasi Anda.
Penanganan Event dengan createPortal
Salah satu manfaat utama dari createPortal adalah ia mempertahankan perilaku event bubbling normal dari React. Ini berarti bahwa event yang berasal dari dalam konten portal akan tetap merambat ke atas pohon komponen React, memungkinkan komponen induk untuk menanganinya.
Namun, penting untuk memahami bagaimana event ditangani ketika mereka melintasi batas portal.
Contoh: Menangani Event di Luar Portal
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function OutsideClickExample() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const portalRoot = document.getElementById('portal-root');
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [dropdownRef]);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle Dropdown</button>
{isOpen && portalRoot && ReactDOM.createPortal(
<div ref={dropdownRef} style={{ position: 'absolute', top: '50px', left: '0', border: '1px solid black', padding: '10px', backgroundColor: 'white' }}>
Konten Dropdown
</div>,
portalRoot
)}
</div>
);
}
export default OutsideClickExample;
Penjelasan:
- Kita menggunakan
refuntuk mengakses elemen dropdown yang dirender di dalam portal. - Kita melampirkan event listener
mousedownkedocumentuntuk mendeteksi klik di luar dropdown. - Di dalam event listener, kita memeriksa apakah klik terjadi di luar dropdown menggunakan
dropdownRef.current.contains(event.target). - Jika klik terjadi di luar dropdown, kita menutupnya dengan mengatur
isOpenmenjadifalse.
Contoh ini menunjukkan cara menangani event yang terjadi di luar konten portal, memungkinkan Anda membuat elemen interaktif yang merespons tindakan pengguna di dokumen sekitarnya.
Kasus Penggunaan Tingkat Lanjut
createPortal tidak terbatas pada modal dan tooltip sederhana. Ini dapat digunakan dalam berbagai skenario tingkat lanjut, termasuk:
- Menu Konteks: Secara dinamis merender menu konteks di dekat kursor mouse saat klik kanan.
- Notifikasi: Menampilkan notifikasi di bagian atas layar, terlepas dari hierarki komponen.
- Popover Kustom: Membuat komponen popover kustom dengan pemosisian dan styling tingkat lanjut.
- Integrasi dengan Pustaka Pihak Ketiga: Menggunakan
createPortaluntuk mengintegrasikan komponen React dengan pustaka pihak ketiga yang memerlukan struktur DOM tertentu.
Contoh: Membuat Menu Konteks
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function ContextMenuExample() {
const [contextMenu, setContextMenu] = useState(null);
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setContextMenu(null);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuRef]);
const handleContextMenu = (event) => {
event.preventDefault();
setContextMenu({
x: event.clientX,
y: event.clientY,
});
};
const portalRoot = document.getElementById('portal-root');
return (
<div onContextMenu={handleContextMenu} style={{ border: '1px solid black', padding: '20px' }}>
Klik kanan di sini untuk membuka menu konteks
{contextMenu && portalRoot && ReactDOM.createPortal(
<div
ref={menuRef}
style={{
position: 'absolute',
top: contextMenu.y,
left: contextMenu.x,
border: '1px solid black',
padding: '10px',
backgroundColor: 'white',
}}
>
<ul>
<li>Opsi 1</li>
<li>Opsi 2</li>
<li>Opsi 3</li>
</ul>
</div>,
portalRoot
)}
</div>
);
}
export default ContextMenuExample;
Penjelasan:
- Kita menggunakan event
onContextMenuuntuk mendeteksi klik kanan pada elemen target. - Kita mencegah menu konteks default muncul menggunakan
event.preventDefault(). - Kita menyimpan koordinat mouse di variabel state
contextMenu. - Kita merender menu konteks di dalam portal, diposisikan pada koordinat mouse.
- Kita menyertakan logika deteksi klik di luar yang sama seperti contoh sebelumnya untuk menutup menu konteks ketika pengguna mengklik di luarnya.
Pertimbangan Aksesibilitas
Saat menggunakan createPortal, sangat penting untuk mempertimbangkan aksesibilitas untuk memastikan bahwa aplikasi Anda dapat digunakan oleh semua orang.
Manajemen Fokus
Ketika sebuah portal terbuka (misalnya, modal), Anda harus memastikan bahwa fokus secara otomatis dipindahkan ke elemen interaktif pertama di dalam portal. Ini membantu pengguna yang menavigasi dengan keyboard atau pembaca layar untuk dengan mudah mengakses konten portal.
Ketika portal ditutup, Anda harus mengembalikan fokus ke elemen yang memicu pembukaan portal. Ini menjaga alur navigasi yang konsisten.
Atribut ARIA
Gunakan atribut ARIA untuk memberikan informasi semantik tentang konten portal. Misalnya, gunakan aria-modal="true" pada elemen modal untuk menunjukkan bahwa itu adalah dialog modal. Gunakan aria-labelledby untuk mengasosiasikan modal dengan judulnya, dan aria-describedby untuk mengasosiasikannya dengan deskripsinya.
Navigasi Keyboard
Pastikan pengguna dapat menavigasi konten portal menggunakan keyboard. Gunakan atribut tabindex untuk mengontrol urutan fokus, dan pastikan semua elemen interaktif dapat dijangkau dengan keyboard.
Pertimbangkan untuk menjebak fokus di dalam portal sehingga pengguna tidak dapat secara tidak sengaja menavigasi ke luarnya. Ini dapat dicapai dengan mendengarkan tombol Tab dan secara terprogram memindahkan fokus ke elemen interaktif pertama atau terakhir di dalam portal.
Contoh: Modal yang Aksesibel
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function AccessibleModal({ children, isOpen, onClose, labelledBy, describedBy }) {
const modalRef = useRef(null);
const firstFocusableElementRef = useRef(null);
const [previouslyFocusedElement, setPreviouslyFocusedElement] = useState(null);
const modalRoot = document.getElementById('modal-root');
useEffect(() => {
if (isOpen) {
// Simpan elemen yang sedang difokuskan sebelum membuka modal.
setPreviouslyFocusedElement(document.activeElement);
// Fokuskan elemen yang dapat difokuskan pertama di modal.
if (firstFocusableElementRef.current) {
firstFocusableElementRef.current.focus();
}
// Jebak fokus di dalam modal.
function handleKeyDown(event) {
if (event.key === 'Tab') {
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
event.preventDefault();
}
} else {
// Tab
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
event.preventDefault();
}
}
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Kembalikan fokus ke elemen yang memiliki fokus sebelum membuka modal.
if(previouslyFocusedElement && previouslyFocusedElement.focus) {
previouslyFocusedElement.focus();
}
};
}
}, [isOpen, previouslyFocusedElement]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div
className="modal-overlay"
onClick={onClose}
aria-modal="true"
aria-labelledby={labelledBy}
aria-describedby={describedBy}
ref={modalRef}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2 id={labelledBy}>Judul Modal</h2>
<p id={describedBy}>Ini adalah konten modal.</p>
<button ref={firstFocusableElementRef} onClick={onClose}>
Tutup
</button>
{children}
</div>
</div>,
modalRoot
);
}
export default AccessibleModal;
Penjelasan:
- Kami menggunakan atribut ARIA seperti
aria-modal,aria-labelledby, danaria-describedbyuntuk memberikan informasi semantik tentang modal. - Kami menggunakan hook
useEffectuntuk mengelola fokus ketika modal dibuka dan ditutup. - Kami menyimpan elemen yang sedang difokuskan sebelum membuka modal dan mengembalikan fokus ke elemen tersebut saat modal ditutup.
- Kami menjebak fokus di dalam modal menggunakan event listener
keydown.
Pertimbangan Internasionalisasi (i18n)
Ketika mengembangkan aplikasi untuk audiens global, internasionalisasi (i18n) adalah pertimbangan penting. Saat menggunakan createPortal, ada beberapa poin yang perlu diingat:
- Arah Teks (RTL/LTR): Pastikan styling Anda mengakomodasi bahasa dari kiri ke kanan (LTR) dan kanan ke kiri (RTL). Ini mungkin melibatkan penggunaan properti logis dalam CSS (misalnya,
margin-inline-startbukanmargin-left) dan mengatur atributdirsecara tepat pada elemen HTML. - Lokalisasi Konten: Semua teks di dalam portal harus dilokalkan ke bahasa pilihan pengguna. Gunakan pustaka i18n (misalnya,
react-intl,i18next) untuk mengelola terjemahan. - Pemformatan Angka dan Tanggal: Format angka dan tanggal sesuai dengan lokal pengguna. API
Intlmenyediakan fungsionalitas untuk ini. - Konvensi Budaya: Waspadai konvensi budaya yang terkait dengan elemen UI. Misalnya, penempatan tombol mungkin berbeda di berbagai budaya.
Contoh: i18n dengan react-intl
import React from 'react';
import { FormattedMessage } from 'react-intl';
function MyComponent() {
return (
<div>
<FormattedMessage id="myComponent.greeting" defaultMessage="Halo, dunia!" />
</div>
);
}
export default MyComponent;
Komponen FormattedMessage dari react-intl mengambil pesan yang diterjemahkan berdasarkan lokal pengguna. Konfigurasikan react-intl dengan terjemahan Anda untuk berbagai bahasa.
Kesalahan Umum dan Solusinya
Meskipun createPortal adalah alat yang kuat, penting untuk menyadari beberapa kesalahan umum dan cara menghindarinya:
- Elemen Root Portal Hilang: Pastikan elemen DOM yang Anda gunakan sebagai root portal sudah ada sebelum komponen yang menggunakan
createPortaldi-mount. Praktik yang baik adalah menempatkannya langsung diindex.html. - Konflik Z-Index: Perhatikan nilai z-index saat memposisikan elemen dengan
createPortal. Gunakan CSS untuk mengelola konteks tumpukan (stacking contexts) dan memastikan konten portal Anda ditampilkan dengan benar. - Masalah Penanganan Event: Pahami bagaimana event merambat melalui portal dan tangani dengan tepat. Gunakan
e.stopPropagation()untuk mencegah event memicu tindakan yang tidak diinginkan. - Kebocoran Memori (Memory Leaks): Bersihkan event listener dan referensi dengan benar saat komponen yang menggunakan
createPortaldi-unmount untuk menghindari kebocoran memori. Gunakan hookuseEffectdengan fungsi pembersihan untuk mencapai ini. - Masalah Pengguliran (Scrolling) yang Tidak Terduga: Portal terkadang dapat mengganggu perilaku pengguliran yang diharapkan dari halaman. Pastikan gaya Anda tidak mencegah pengguliran dan elemen modal tidak menyebabkan halaman melompat atau perilaku pengguliran yang tidak terduga saat dibuka dan ditutup.
Kesimpulan
React.createPortal adalah alat yang berharga untuk menciptakan UI yang fleksibel, dapat diakses, dan dapat dipelihara di React. Dengan memahami tujuan, penggunaan, dan teknik tingkat lanjut untuk menangani event dan aksesibilitas, Anda dapat memanfaatkan kekuatannya untuk membangun aplikasi web yang kompleks dan menarik yang memberikan pengalaman pengguna yang unggul untuk audiens global. Ingatlah untuk mempertimbangkan praktik terbaik internasionalisasi dan aksesibilitas untuk memastikan aplikasi Anda inklusif dan dapat digunakan oleh semua orang.
Dengan mengikuti pedoman dan contoh dalam panduan ini, Anda dapat dengan percaya diri menggunakan createPortal untuk menyelesaikan tantangan UI umum dan menciptakan pengalaman web yang menakjubkan.